/* eslint-disable no-func-assign */
/**
 * Flatten array, one level deep.
 *
 * @param {Array<?>} arr
 *
 * @return {Array<?>}
 */
function flatten(arr) {
  return Array.prototype.concat.apply([], arr)
}

let nativeToString = Object.prototype.toString
let nativeHasOwnProperty = Object.prototype.hasOwnProperty
function isUndefined(obj) {
  return obj === undefined
}
function isDefined(obj) {
  return obj !== undefined
}
function isNil(obj) {
  return obj == null
}
function isArray(obj) {
  return nativeToString.call(obj) === '[object Array]'
}
function isObject(obj) {
  return nativeToString.call(obj) === '[object Object]'
}
function isNumber(obj) {
  return nativeToString.call(obj) === '[object Number]'
}
function isFunction(obj) {
  let tag = nativeToString.call(obj)
  return (
    tag === '[object Function]' ||
    tag === '[object AsyncFunction]' ||
    tag === '[object GeneratorFunction]' ||
    tag === '[object AsyncGeneratorFunction]' ||
    tag === '[object Proxy]'
  )
}
function isString(obj) {
  return nativeToString.call(obj) === '[object String]'
}
/**
 * Ensure collection is an array.
 *
 * @param {Object} obj
 */

function ensureArray(obj) {
  if (isArray(obj)) {
    return
  }

  throw new Error('must supply array')
}
/**
 * Return true, if target owns a property with the given key.
 *
 * @param {Object} target
 * @param {String} key
 *
 * @return {Boolean}
 */

function has(target, key) {
  return nativeHasOwnProperty.call(target, key)
}

/**
 * Find element in collection.
 *
 * @param  {Array|Object} collection
 * @param  {Function|Object} matcher
 *
 * @return {Object}
 */

function find(collection, matcher) {
  matcher = toMatcher(matcher)
  let match
  forEach(collection, function (val, key) {
    if (matcher(val, key)) {
      match = val
      return false
    }
  })
  return match
}
/**
 * Find element index in collection.
 *
 * @param  {Array|Object} collection
 * @param  {Function} matcher
 *
 * @return {Object}
 */

function findIndex(collection, matcher) {
  matcher = toMatcher(matcher)
  let idx = isArray(collection) ? -1 : undefined
  forEach(collection, function (val, key) {
    if (matcher(val, key)) {
      idx = key
      return false
    }
  })
  return idx
}
/**
 * Find element in collection.
 *
 * @param  {Array|Object} collection
 * @param  {Function} matcher
 *
 * @return {Array} result
 */

function filter(collection, matcher) {
  let result = []
  forEach(collection, function (val, key) {
    if (matcher(val, key)) {
      result.push(val)
    }
  })
  return result
}
/**
 * Iterate over collection; returning something
 * (non-undefined) will stop iteration.
 *
 * @param  {Array|Object} collection
 * @param  {Function} iterator
 *
 * @return {Object} return result that stopped the iteration
 */

function forEach(collection, iterator) {
  let val, result

  if (isUndefined(collection)) {
    return
  }

  let convertKey = isArray(collection) ? toNum : identity

  for (let key in collection) {
    if (has(collection, key)) {
      val = collection[key]
      result = iterator(val, convertKey(key))

      if (result === false) {
        return val
      }
    }
  }
}
/**
 * Return collection without element.
 *
 * @param  {Array} arr
 * @param  {Function} matcher
 *
 * @return {Array}
 */

function without(arr, matcher) {
  if (isUndefined(arr)) {
    return []
  }

  ensureArray(arr)
  matcher = toMatcher(matcher)
  return arr.filter(function (el, idx) {
    return !matcher(el, idx)
  })
}
/**
 * Reduce collection, returning a single result.
 *
 * @param  {Object|Array} collection
 * @param  {Function} iterator
 * @param  {Any} result
 *
 * @return {Any} result returned from last iterator
 */

function reduce(collection, iterator, result) {
  forEach(collection, function (value, idx) {
    result = iterator(result, value, idx)
  })
  return result
}
/**
 * Return true if every element in the collection
 * matches the criteria.
 *
 * @param  {Object|Array} collection
 * @param  {Function} matcher
 *
 * @return {Boolean}
 */

function every(collection, matcher) {
  return !!reduce(
    collection,
    function (matches, val, key) {
      return matches && matcher(val, key)
    },
    true,
  )
}
/**
 * Return true if some elements in the collection
 * match the criteria.
 *
 * @param  {Object|Array} collection
 * @param  {Function} matcher
 *
 * @return {Boolean}
 */

function some(collection, matcher) {
  return !!find(collection, matcher)
}
/**
 * Transform a collection into another collection
 * by piping each member through the given fn.
 *
 * @param  {Object|Array}   collection
 * @param  {Function} fn
 *
 * @return {Array} transformed collection
 */

function map(collection, fn) {
  let result = []
  forEach(collection, function (val, key) {
    result.push(fn(val, key))
  })
  return result
}
/**
 * Get the collections keys.
 *
 * @param  {Object|Array} collection
 *
 * @return {Array}
 */

function keys(collection) {
  return (collection && Object.keys(collection)) || []
}
/**
 * Shorthand for `keys(o).length`.
 *
 * @param  {Object|Array} collection
 *
 * @return {Number}
 */

function size(collection) {
  return keys(collection).length
}
/**
 * Get the values in the collection.
 *
 * @param  {Object|Array} collection
 *
 * @return {Array}
 */

function values(collection) {
  return map(collection, function (val) {
    return val
  })
}
/**
 * Group collection members by attribute.
 *
 * @param  {Object|Array} collection
 * @param  {Function} extractor
 *
 * @return {Object} map with { attrValue => [ a, b, c ] }
 */

function groupBy(collection, extractor) {
  let grouped = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}
  extractor = toExtractor(extractor)
  forEach(collection, function (val) {
    let discriminator = extractor(val) || '_'
    let group = grouped[discriminator]

    if (!group) {
      group = grouped[discriminator] = []
    }

    group.push(val)
  })
  return grouped
}
function uniqueBy(extractor) {
  extractor = toExtractor(extractor)
  let grouped = {}

  // eslint-disable-next-line no-var
  for (var _len = arguments.length, collections = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
    collections[_key - 1] = arguments[_key]
  }

  forEach(collections, function (c) {
    return groupBy(c, extractor, grouped)
  })
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  let result = map(grouped, function (val, key) {
    return val[0]
  })
  return result
}
let unionBy = uniqueBy
/**
 * Sort collection by criteria.
 *
 * @param  {Object|Array} collection
 * @param  {String|Function} extractor
 *
 * @return {Array}
 */

function sortBy(collection, extractor) {
  extractor = toExtractor(extractor)
  let sorted = []
  forEach(collection, function (value, key) {
    let disc = extractor(value, key)
    let entry = {
      d: disc,
      v: value,
    }

    for (let idx = 0; idx < sorted.length; idx++) {
      let d = sorted[idx].d

      if (disc < d) {
        sorted.splice(idx, 0, entry)
        return
      }
    } // not inserted, append (!)

    sorted.push(entry)
  })
  return map(sorted, function (e) {
    return e.v
  })
}
/**
 * Create an object pattern matcher.
 *
 * @example
 *
 * const matcher = matchPattern({ id: 1 });
 *
 * let element = find(elements, matcher);
 *
 * @param  {Object} pattern
 *
 * @return {Function} matcherFn
 */

function matchPattern(pattern) {
  return function (el) {
    return every(pattern, function (val, key) {
      return el[key] === val
    })
  }
}

function toExtractor(extractor) {
  return isFunction(extractor)
    ? extractor
    : function (e) {
        return e[extractor]
      }
}

function toMatcher(matcher) {
  return isFunction(matcher)
    ? matcher
    : function (e) {
        return e === matcher
      }
}

function identity(arg) {
  return arg
}

function toNum(arg) {
  return Number(arg)
}

/**
 * Debounce fn, calling it only once if the given time
 * elapsed between calls.
 *
 * Lodash-style the function exposes methods to `#clear`
 * and `#flush` to control internal behavior.
 *
 * @param  {Function} fn
 * @param  {Number} timeout
 *
 * @return {Function} debounced function
 */
function debounce(fn, timeout) {
  let timer
  let lastArgs
  let lastThis
  let lastNow

  function fire(force) {
    let now = Date.now()
    let scheduledDiff = force ? 0 : lastNow + timeout - now

    if (scheduledDiff > 0) {
      return schedule(scheduledDiff)
    }

    fn.apply(lastThis, lastArgs)
    clear()
  }

  function schedule(timeout) {
    timer = setTimeout(fire, timeout)
  }

  function clear() {
    if (timer) {
      clearTimeout(timer)
    }

    timer = lastNow = lastArgs = lastThis = undefined
  }

  function flush() {
    if (timer) {
      fire(true)
    }

    clear()
  }

  function callback() {
    lastNow = Date.now()

    // eslint-disable-next-line no-var
    for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
      args[_key] = arguments[_key]
    }

    lastArgs = args
    // eslint-disable-next-line @typescript-eslint/no-this-alias
    lastThis = this // ensure an execution is scheduled

    if (!timer) {
      schedule(timeout)
    }
  }

  callback.flush = flush
  callback.cancel = clear
  return callback
}
/**
 * Throttle fn, calling at most once
 * in the given interval.
 *
 * @param  {Function} fn
 * @param  {Number} interval
 *
 * @return {Function} throttled function
 */

function throttle(fn, interval) {
  let throttling = false
  return function () {
    if (throttling) {
      return
    }

    fn.apply(void 0, arguments)
    throttling = true
    setTimeout(function () {
      throttling = false
    }, interval)
  }
}
/**
 * Bind function against target <this>.
 *
 * @param  {Function} fn
 * @param  {Object}   target
 *
 * @return {Function} bound function
 */

function bind(fn, target) {
  return fn.bind(target)
}

function _typeof(obj) {
  '@babel/helpers - typeof'

  if (typeof Symbol === 'function' && typeof Symbol.iterator === 'symbol') {
    _typeof = function (obj) {
      return typeof obj
    }
  } else {
    _typeof = function (obj) {
      return obj && typeof Symbol === 'function' && obj.constructor === Symbol && obj !== Symbol.prototype
        ? 'symbol'
        : typeof obj
    }
  }

  return _typeof(obj)
}

function _extends() {
  _extends =
    Object.assign ||
    function (target) {
      for (let i = 1; i < arguments.length; i++) {
        let source = arguments[i]

        for (let key in source) {
          if (Object.prototype.hasOwnProperty.call(source, key)) {
            target[key] = source[key]
          }
        }
      }

      return target
    }

  return _extends.apply(this, arguments)
}

/**
 * Convenience wrapper for `Object.assign`.
 *
 * @param {Object} target
 * @param {...Object} others
 *
 * @return {Object} the target
 */

function assign(target) {
  let others
  for (let _len = arguments.length, others = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
    others[_key - 1] = arguments[_key]
  }

  // eslint-disable-next-line no-undef
  return _extends.apply(void 0, [target].concat(others))
}
/**
 * Sets a nested property of a given object to the specified value.
 *
 * This mutates the object and returns it.
 *
 * @param {Object} target The target of the set operation.
 * @param {(string|number)[]} path The path to the nested value.
 * @param {any} value The value to set.
 */

function set(target, path, value) {
  let currentTarget = target
  forEach(path, function (key, idx) {
    if (typeof key !== 'number' && typeof key !== 'string') {
      throw new Error('illegal key type: ' + _typeof(key) + '. Key should be of type number or string.')
    }

    if (key === 'constructor') {
      throw new Error('illegal key: constructor')
    }

    if (key === '__proto__') {
      throw new Error('illegal key: __proto__')
    }

    let nextKey = path[idx + 1]
    let nextTarget = currentTarget[key]

    if (isDefined(nextKey) && isNil(nextTarget)) {
      nextTarget = currentTarget[key] = isNaN(+nextKey) ? {} : []
    }

    if (isUndefined(nextKey)) {
      if (isUndefined(value)) {
        delete currentTarget[key]
      } else {
        currentTarget[key] = value
      }
    } else {
      currentTarget = nextTarget
    }
  })
  return target
}
/**
 * Gets a nested property of a given object.
 *
 * @param {Object} target The target of the get operation.
 * @param {(string|number)[]} path The path to the nested value.
 * @param {any} [defaultValue] The value to return if no value exists.
 */

function get(target, path, defaultValue) {
  let currentTarget = target
  forEach(path, function (key) {
    // accessing nil property yields <undefined>
    if (isNil(currentTarget)) {
      currentTarget = undefined
      return false
    }

    currentTarget = currentTarget[key]
  })
  return isUndefined(currentTarget) ? defaultValue : currentTarget
}
/**
 * Pick given properties from the target object.
 *
 * @param {Object} target
 * @param {Array} properties
 *
 * @return {Object} target
 */

function pick(target, properties) {
  let result = {}
  let obj = Object(target)
  forEach(properties, function (prop) {
    if (prop in obj) {
      result[prop] = target[prop]
    }
  })
  return result
}
/**
 * Pick all target properties, excluding the given ones.
 *
 * @param {Object} target
 * @param {Array} properties
 *
 * @return {Object} target
 */

function omit(target, properties) {
  let result = {}
  let obj = Object(target)
  forEach(obj, function (prop, key) {
    if (properties.indexOf(key) === -1) {
      result[key] = prop
    }
  })
  return result
}
/**
 * Recursively merge `...sources` into given target.
 *
 * Does support merging objects; does not support merging arrays.
 *
 * @param {Object} target
 * @param {...Object} sources
 *
 * @return {Object} the target
 */
function merge(target) {
  for (
    let _len2 = arguments.length, sources = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1;
    _key2 < _len2;
    _key2++
  ) {
    sources[_key2 - 1] = arguments[_key2]
  }

  // eslint-disable-next-line no-undef
  if (!sources.length) {
    return target
  }

  // eslint-disable-next-line no-undef
  forEach(sources, function (source) {
    // skip non-obj sources, i.e. null
    if (!source || !isObject(source)) {
      return
    }

    forEach(source, function (sourceVal, key) {
      if (key === '__proto__') {
        return
      }

      let targetVal = target[key]

      if (isObject(sourceVal)) {
        if (!isObject(targetVal)) {
          // override target[key] with object
          targetVal = {}
        }

        target[key] = merge(targetVal, sourceVal)
      } else {
        target[key] = sourceVal
      }
    })
  })
  return target
}

export {
  assign,
  bind,
  debounce,
  ensureArray,
  every,
  filter,
  find,
  findIndex,
  flatten,
  forEach,
  get,
  groupBy,
  has,
  isArray,
  isDefined,
  isFunction,
  isNil,
  isNumber,
  isObject,
  isString,
  isUndefined,
  keys,
  map,
  matchPattern,
  merge,
  omit,
  pick,
  reduce,
  set,
  size,
  some,
  sortBy,
  throttle,
  unionBy,
  uniqueBy,
  values,
  without,
}
